##############################################################################
# 
# Zope Public License (ZPL) Version 1.0
# -------------------------------------
# 
# Copyright (c) Digital Creations.  All rights reserved.
# 
# This license has been certified as Open Source(tm).
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 
# 1. Redistributions in source code must retain the above copyright
#    notice, this list of conditions, and the following disclaimer.
# 
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions, and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
# 
# 3. Digital Creations requests that attribution be given to Zope
#    in any manner possible. Zope includes a "Powered by Zope"
#    button that is installed by default. While it is not a license
#    violation to remove this button, it is requested that the
#    attribution remain. A significant investment has been put
#    into Zope, and this effort will continue if the Zope community
#    continues to grow. This is one way to assure that growth.
# 
# 4. All advertising materials and documentation mentioning
#    features derived from or use of this software must display
#    the following acknowledgement:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    In the event that the product being advertised includes an
#    intact Zope distribution (with copyright and license included)
#    then this clause is waived.
# 
# 5. Names associated with Zope or Digital Creations must not be used to
#    endorse or promote products derived from this software without
#    prior written permission from Digital Creations.
# 
# 6. Modified redistributions of any form whatsoever must retain
#    the following acknowledgment:
# 
#      "This product includes software developed by Digital Creations
#      for use in the Z Object Publishing Environment
#      (http://www.zope.org/)."
# 
#    Intact (re-)distributions of any official Zope release do not
#    require an external acknowledgement.
# 
# 7. Modifications are encouraged but must be packaged separately as
#    patches to official Zope releases.  Distributions that do not
#    clearly separate the patches from the original work must be clearly
#    labeled as unofficial distributions.  Modifications which do not
#    carry the name Zope may be packaged in any form, as long as they
#    conform to all of the clauses above.
# 
# 
# Disclaimer
# 
#   THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY
#   EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
#   PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL DIGITAL CREATIONS OR ITS
#   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#   LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
#   USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
#   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#   OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
#   OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
#   SUCH DAMAGE.
# 
# 
# This software consists of contributions made by Digital Creations and
# many individuals on behalf of Digital Creations.  Specific
# attributions are listed in the accompanying credits file.
# 
##############################################################################
__doc__='''ZOracleDA Simple Pool
$Id $''' 
__version__='$Revision: 1.8 $'[11:-2]

import thread
import threading
import sys
from zExceptions import BadRequest
from DCOracle2 import dco2
from Products.PythonScripts.standard import html_quote
import DA

import logging
logger = logging.getLogger('da_simple_pool.py')

# pool states
DA_POOL_CLOSED  = 1
DA_POOL_CLOSING = -1
DA_POOL_OPEN    = 0

class DASimplePoolError(dco2.Error):
    pass

class DASimplePool(object):
    """ Simple pool that uses only one object per thread
    """
    
    def __init__(self, factory, dsn, unique_id, min_size=1):
        self._status    = DA_POOL_OPEN     # pool status
        self._factory   = factory
        self._dsn       = dsn              # connectstring
        self._unique_id = unique_id        # unique id of DA persistent object
        self._lock      = threading.Lock()
        self._min_size  = int(min_size)
        
        # min_size shouldn't be larger than number of threads
        zserver_threads = DA.getThreadsNo()
        if zserver_threads < self._min_size:
            self._min_size = zserver_threads
        
        self._pool_avail = []   # list of available resource managers
        self._pool_used  = {}   # dictionary of resource managers in use
        
        # pool initialization
        for i in range(self._min_size):
            self._pool_avail.append(self._connect()) 
        logger.info('Created pool of \'%s\' connections to: \'%s\'' \
                             % (self._min_size, unique_id))
    
    def get_pool_size(self):
        """ Returns the number of objects in pool
        """
        self._lock.acquire()
        try:
            return len(self._pool_avail)+len(self._pool_used.keys())
        finally:
            self._lock.release()
    
    def _connect(self):
        """ Create instance of ResourceManager
        """
        rm = None
        try:
            try:
                DB = self._factory()
                rm = DB(self._dsn, self._unique_id)
            except:
                t, v, tb = sys.exc_info()
                err_info_str = '%s %s' % (t,v)
                err_str = '''<strong>Error: </strong><CODE>%s</CODE><br>\n
                             %s<br>
                             <!--\n%s\n%s\n-->\n'''
                raise BadRequest, (err_str
                    % (self._dsn,
                       html_quote(err_info_str), t,v)), tb
        finally: tb=None
        return rm
        
    def getconn(self):
        """ Get resource manager from the pool or create additional 
            one if needed
        """
        key = thread.get_ident()
        rm = None
        
        self._lock.acquire()
        try:
            if self._status != DA_POOL_OPEN:
                raise DASimplePoolError("Pool is closed!")
            
            if key in self._pool_used.keys():
                logger.debug('%s: getconn rm: %s returned from the _pool_used' % (self._unique_id, key))
                return self._pool_used[key]
                
            if self._pool_avail:            
                rm = self._pool_avail.pop()
            else:
                # there is no upper limit
                # of rm's that may be created but it shouldn't be possible                
                # to have poll size larger than number of zope threads
                rm = self._connect()
                logger.debug('%s: getconn rm created: %s' % (self._unique_id, key))
            logger.debug('%s: getconn rm: %s added to _pool_used' % (self._unique_id, key))
            self._pool_used[key] = rm
        finally:
            self._lock.release()
        return rm
        
    def putconn(self, close=False):
        """ Return resource manager to the pool or dispose it if
            there is more resource managers than min_size
        """
        key = thread.get_ident()
        self._lock.acquire()
        try:
            if key in self._pool_used.keys():
                rm = self._pool_used[key]
                if (self._status != DA_POOL_OPEN \
                  or len(self._pool_avail) >= self._min_size \
                  or close) and rm.isOpen():
                    logger.debug('%s: putconn rm: %s closed' % (self._unique_id, key))
                    rm.close()
                else:
                    logger.debug('%s: putconn rm: %s added to avail' % (self._unique_id, key))
                    self._pool_avail.append(rm)
                del self._pool_used[key]
                logger.debug('%s: putconn rm: %s removed from _pool_used' % (self._unique_id, key))
        finally:
            self._lock.release()
        
    def dispose(self):
        """ Close all available (not currently used) resource managers. 
            Status DA_POOL_CLOSING is used to mark that there
            are resource managers still in use that are not closed yet.
        """
        logger.debug('%s: Call to dispose' % (self._unique_id))
        if self._status == DA_POOL_CLOSED:
            raise DASimplePoolError("ResourceManager pool is closed")
        
        # put the current connection if any
        self.putconn(close=True)
        
        self._lock.acquire()
        try:
            for rm in self._pool_avail:
                logger.info('%s: Pool dispose - closing RM from pool of available RM\'s.' % (self._unique_id))
                rm.close()
            self._pool_avail = []
                    
            if len(self._pool_used.keys())>0:
                logger.info('%s: Pool dispose - there are RM\'s that are still in use: %s' % (self._unique_id, str(self._pool_used.keys())))
                self._status = DA_POOL_CLOSING # if there still are rm's in use
            else:
                logger.info('%s: Pool dispose - all RM\'s closed.' % (self._unique_id))
                self._status = DA_POOL_CLOSED
        finally:
            self._lock.release()
        return self._status